package org.zxt.web.context;

import com.alibaba.fastjson.JSONObject;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.io.ResolverUtil;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import org.springframework.util.StringUtils;
import org.zxt.web.buss.config.Conf;
import org.zxt.web.context.annotation.RequestMapping;
import org.zxt.web.context.handler.*;
import org.zxt.web.context.handler.parser.*;
import org.zxt.web.nacos.NacosDiscoveryClient;
import org.zxt.web.nio.IHTTPSession;
import org.zxt.web.nio.Response;
import org.zxt.web.nio.Status;

import javax.annotation.Resource;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;

import static org.zxt.web.nio.NioHTTPD.MIME_APPLICATION_JSON;
import static org.zxt.web.nio.NioHTTPD.MIME_HTML;

@Slf4j
public class WebContext {
    Router<RequestMapperHandler>[] routers;
    List<Interceptor> interceptors;
    Map<String, ExceptionHandler> exceptionHandlers;
    List<ArgumentParser> argumentParserList;
    Map<String, ResultParser> resultParser;
    @Getter
    Map<String, Object> beanMap;
    String pkg;

    public WebContext() {
        routers = new Router[16];
        interceptors = new ArrayList<>();
        exceptionHandlers = new TreeMap<>();
        argumentParserList = new ArrayList<>(9);
        argumentParserList.add(new IHTTPSessionParser());
        argumentParserList.add(new PageParamParser());
        argumentParserList.add(new RequestBodyParser());
        argumentParserList.add(new RequestHeaderParser());
        argumentParserList.add(new RequestParamParser());
        argumentParserList.add(new RequestUriParser());
        argumentParserList.add(new WebContextParser());
        argumentParserList.add(new PathVariableParser());
        argumentParserList.add(new ObjectParamParser());

        resultParser = new HashMap<>();
        resultParser.put(String.class.getName(), Object::toString);
        resultParser.put(Integer.class.getName(), Object::toString);
        resultParser.put(Boolean.class.getName(), Object::toString);
        beanMap = new TreeMap<>();
    }
    public void parseHandler(Object target){
        Class<?> clazz = target.getClass();
        RequestMapping requestMapping = clazz.getAnnotation(RequestMapping.class);
        Method[] ms = clazz.getDeclaredMethods();

        for(Method m : ms){
            RequestMapping mapping = m.getAnnotation(RequestMapping.class);
            if(mapping == null){
                continue;
            }
            String path = requestMapping.value() + mapping.value();
            Parameter[] ps = m.getParameters();
            ArgumentParser[] aps = new ArgumentParser[ps.length];
            for(int i=0;i<ps.length;i++){
                Parameter p = ps[i];
                for(ArgumentParser ap: argumentParserList){
                    if(ap.support(p)){
                        aps[i] = ap;
                        break;
                    }
                }
            }
            RequestMapperHandler requestMapperHandler = new RequestMapperHandler() {
                @Override
                public Object[] parseArguments(WebContext webContext, IHTTPSession session) {
                    Parameter[] ps = m.getParameters();
                    Object[] params = new Object[aps.length];
                    for(int i=0;i<params.length;i++){
                        ArgumentParser ap = aps[i];
                        params[i] = ap.parse(ps[i], webContext, session);
                    }
                    return params;
                }

                @Override
                public Method getInvoker() {
                    return m;
                }

                @Override
                public Object getTarget() {
                    return target;
                }
            };
            registeMappingHandlers(mapping.method(), path, requestMapperHandler);
        }
    }

//    public Map<String, Object> getBeanMap() {
//        return beanMap;
//    }
    public <T> T getBean(String key) {
        return (T)beanMap.get(key);
    }
    public <T> T getBean(Class<T> clazz) {
        return (T)beanMap.get(clazz.getName());
    }

    private AtomicInteger count = new AtomicInteger(0);

    public static Object InitBean(Resource resource, Map<String, Object> classBeans, Object target, Method m) throws InvocationTargetException, IllegalAccessException {
        if(resource.type() == ResourceType.Bean.class) {
            Parameter[] ps = m.getParameters();
            Object bean = null;
            if (ps == null || ps.length == 0) {
                bean = m.invoke(target);
            } else {
                Object[] params = new Object[ps.length];
                for (int i=0;i<ps.length;i++) {
                    Parameter p = ps[i];
                    if (!classBeans.containsKey(p.getType().getName())) {
                        return null;
                    }
                    params[i] = classBeans.get(p.getType().getName());
                }
                bean = m.invoke(target, params);
            }
            classBeans.put(m.getReturnType().getName(), bean);
            String beanName = resource.name();
            if(!StringUtils.hasText(beanName)){
                beanName = m.getName();
            }
            Object old = classBeans.put(beanName, bean);
            if(old != null){
                throw new RuntimeException(beanName+" already include a same name");
            }
            return bean;
        } else if(resource.type() == ResourceType.Router.class){
            return new AbstractMap.SimpleEntry<>(m, target);
        }
        return null;
    }
    public static Object InitBean(Resource resource, Map<String, Object> classBeans, Class<?> clazz, CglibCallback call) {
        Object obj = null;
        if(resource.type() == ResourceType.Proxy.class){
            String proxyBean = resource.lookup();
            if(!classBeans.containsKey(proxyBean)){
                return null;
            }
            Function<Object, Class<?>> func = (Function<Object, Class<?>>) classBeans.get(proxyBean);
            if(func == null){
                //log.debug(proxyBean +" not exists");
                throw new RuntimeException(proxyBean +" not exists");
            }
            obj = func.apply(clazz);
        } else {
            obj = createCglibProxy(clazz, call);//clazz.newInstance();
        }
        Class<?>[] clases = clazz.getInterfaces();
        for(Class<?> c: clases){
            classBeans.put(c.getName(), obj);
        }
        classBeans.put(clazz.getName(), obj);
        String beanName = resource.name();
        if(!StringUtils.hasText(beanName)){
            beanName = clazz.getSimpleName();
        }
        Object old = classBeans.put(beanName, obj);
        if(old != null){
            throw new RuntimeException(beanName+" already include a same name");
        }
        return obj;
    }

    private static Object createCglibProxy(Class<?> clazz, CglibCallback call) {
//        Enhancer enhancer = new Enhancer();
//        enhancer.setSuperclass(clazz);
//        enhancer.setCallback(call);
//        return enhancer.create();
        try {
            return clazz.newInstance();
        } catch (Exception ex){
            throw new RuntimeException(ex);
        }
    }

    public void registryNacos() {
        Conf env = this.getBean(Conf.class);
        NacosDiscoveryClient client = new NacosDiscoveryClient(env.getString("nacos.discovery.server-addr"), new TreeMap<String, String>() {{
            put("namespaceId", env.getString("nacos.discovery.namespace"));
            put("serviceName", env.getString("server.application-name"));
            put("port", env.getString("server.port"));
            put("ip", "192.168.4.24");
        }});
        this.beanMap.put(NacosDiscoveryClient.class.getName(), client);
        if(env.getBooleanValue("nacos.discovery.registry-enabled")) {
            client.register();
        }
    }

    public static class CglibCallback implements MethodInterceptor {
        private WebContext webContext;
        public CglibCallback(WebContext webContext){
            this.webContext = webContext;
        }

        @Override
        public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
//            for (Interceptor interceptor : webContext.interceptors) {
//                interceptor.before(obj, method, args, proxy);
//            }
//            Object res = proxy.invokeSuper(obj, args);
//            for (Interceptor interceptor : webContext.interceptors) {
//                interceptor.after(res, obj, method, args, proxy);
//            }
//            return res;
            return proxy.invokeSuper(obj, args);
        }
    }

    public static Object InjectBean(/*Resource resource,*/ Map<String, Object> classBeans, Object bean, Field f) throws IllegalAccessException {
        if(!classBeans.containsKey(f.getType().getName())){
            return null;
        }
        Object xbean = classBeans.get(f.getType().getName());
        f.setAccessible(true);
        f.set(bean, xbean);
        f.setAccessible(false);
        return xbean;
    }

    public RequestMapperHandler getMappingHandler(IHTTPSession session) {
        if(session == null || session.getMethod() == null){
            log.warn("session is null");
            return null;
        }
        int i = session.getMethod().ordinal();
        if(routers[i]==null){
            routers[i] = new Router<>();
        }
        return routers[i].get(session, session.getUri());
    }

    public ExceptionHandler getExceptionHandler(Throwable e) {
        return exceptionHandlers.getOrDefault(e.getClass().getName(), (webContext, session, e1) -> {
            e1.printStackTrace();
            StringBuilder sb = new StringBuilder(e1.toString());
            StackTraceElement[] ss = e1.getStackTrace();
            for(StackTraceElement s:ss){
                sb.append("-").append(s.getClassName()).append("@").append(s.getMethodName()).append("----").append(s.getFileName()).append(":").append(s.getLineNumber()).append("\r\n");
            }
            return new Response(session, Status.INTERNAL_ERROR, MIME_APPLICATION_JSON, "{\"error\":\"something wrong\", \"strace\":\""+sb.toString()+"\"}");
        });
    }

    public void registeArgumentParser(ArgumentParser argumentParser){
        argumentParserList.add(argumentParser);
    }

    public void registeInterceptor(Interceptor interceptor){
        interceptors.add(interceptor);
    }

    public void registeMappingHandlers(org.zxt.web.nio.Method method, String path, RequestMapperHandler handler){
        if(routers[method.ordinal()] == null){
            routers[method.ordinal()] = new Router<>();
        }
        RequestMapperHandler old = routers[method.ordinal()].put(path, handler);
        if(old != null){
            throw new RuntimeException(path+" already exists!");
        }
    }

    public void registeExceptionHandlers(Class<? extends Throwable> clazz, ExceptionHandler handler){
        exceptionHandlers.put(clazz.getName(), handler);
    }

    public Response service(IHTTPSession session) {
        RequestMapperHandler mappingHandler = this.getMappingHandler(session);
        if(mappingHandler == null){
            return new Response(session, Status.NOT_FOUND, MIME_HTML, "404");
        }
        Object[] params = mappingHandler.parseArguments(this, session);
        try {
            Method method = mappingHandler.getInvoker();
            Object result = method.invoke(mappingHandler.getTarget(), params);
            if (result instanceof Response){
                return (Response) result;
            }
            String res = this.getResultParser(result);
            return new Response(session, Status.OK, MIME_APPLICATION_JSON, res);
        } catch (Throwable e) {
            ExceptionHandler exceptionHandler = this.getExceptionHandler(e);
            Object result = exceptionHandler.handle(this, session, e);
            if (result instanceof Response){
                return (Response) result;
            }
            return new Response(session, Status.INTERNAL_ERROR, MIME_APPLICATION_JSON, JSONObject.toJSONString(result));
        }
    }
    JSONObjectParser jsonObjectParser = new JSONObjectParser();
    private String getResultParser(Object result) {
        if(result == null){
            return null;
        }
       return resultParser.getOrDefault(result.getClass(), jsonObjectParser).parser(result);
    }

    public void scanActionsFromBeans() {
        this.beanMap.values().stream().distinct().forEach(o->{
            Resource resource = o.getClass().getAnnotation(Resource.class);
            if(resource != null && resource.type() == ResourceType.Action.class){
                parseHandler(o);
            }
        });
    }

    public void initialBeans() throws Exception {
        ResolverUtil<Class<?>> resolverUtil = new ResolverUtil();
        resolverUtil.find(new ResolverUtil.IsA(Object.class), pkg);
        Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
        List<Map.Entry<Object, Object>> errors = new ArrayList<>();
        CglibCallback cglibCallback = new CglibCallback(this);
        initBeans(this.getBeanMap(), errors, mapperSet.iterator(), cglibCallback);
        while (!errors.isEmpty()){
            List<Map.Entry<Object, Object>> nerrors = new ArrayList<>();
            initBeansForErrors(this.getBeanMap(), errors, nerrors, mapperSet.iterator(), cglibCallback);
            errors = nerrors;
        }
    }

    private static void initBeansForErrors(Map<String, Object> classBeans, List<Map.Entry<Object, Object>> errors, List<Map.Entry<Object, Object>> nerrors, Iterator<Class<? extends Class<?>>> var5, CglibCallback cglibCallback) throws Exception{
        for(Map.Entry<Object, Object> entry : errors){
            Object o = entry.getKey();
            if (o instanceof Method){
                Object mbean = WebContext.InitBean(((Method) o).getAnnotation(Resource.class), classBeans, entry.getValue(), (Method) o);
                if(mbean == null){
                    nerrors.add(new AbstractMap.SimpleEntry<>(o, entry.getValue()));
                }
            } else if(o instanceof Field){
                Object mbean = WebContext.InjectBean(/*((Field) o).getAnnotation(Resource.class),*/ classBeans, entry.getValue(), (Field) o);
                if(mbean == null){
                    nerrors.add(new AbstractMap.SimpleEntry<>(o, entry.getValue()));
                }
            } else if(o instanceof Class){
                Object bean = WebContext.InitBean(((Class<?>) o).getAnnotation(Resource.class), classBeans, (Class)o, cglibCallback);
                if(bean == null){
                    nerrors.add(new AbstractMap.SimpleEntry<>((Class)o, entry.getValue()));
                }
            }
        }
    }

    private static void initBeans(Map<String, Object> classBeans, List<Map.Entry<Object, Object>> errors, Iterator<Class<? extends Class<?>>> var5, CglibCallback cglibCallback) throws Exception{
        while(var5.hasNext()) {
            Class<? extends Class<?>> clazz = var5.next();
            Resource resource = clazz.getAnnotation(Resource.class);
            if(resource!=null){
                Object bean = WebContext.InitBean(resource, classBeans, clazz, cglibCallback);
                if(bean == null){
                    errors.add(new AbstractMap.SimpleEntry<>(clazz, resource));
                }
                log.debug(clazz+":"+resource.type());
                Method[] ms = clazz.getDeclaredMethods();
                for(Method m:ms){
                    Resource methodResource = m.getAnnotation(Resource.class);
                    if(methodResource != null){
                        Object mbean = WebContext.InitBean(resource, classBeans, bean, m);
                        if(mbean == null){
                            errors.add(new AbstractMap.SimpleEntry<>(m, bean));
                        }
                        log.debug("\tmethod:"+m.getName()+"\t"+methodResource.type());
                    }
                }
                Field[] fs = clazz.getDeclaredFields();
                for(Field f:fs){
                    Resource fieldResource = f.getAnnotation(Resource.class);
                    if(fieldResource!=null){
                        Object mbean = WebContext.InjectBean(/*resource,*/ classBeans, bean, f);
                        if(mbean == null){
                            errors.add(new AbstractMap.SimpleEntry<>(f, bean));
                        }
                        log.debug("\tfield:"+f.getName()+ f.getType()+"\t"+fieldResource.type()+"\t"+fieldResource.lookup());
                    }
                }
                if(clazz.getSuperclass() != null){
                    Field[] fs1 = clazz.getSuperclass().getDeclaredFields();
                    for(Field f:fs1){
                        Resource methodResource = f.getAnnotation(Resource.class);
                        if(methodResource!=null){
                            Object mbean = WebContext.InjectBean(/*resource,*/ classBeans, bean, f);
                            if(mbean == null){
                                errors.add(new AbstractMap.SimpleEntry<>(f, bean));
                            }
                            log.debug("\tfield:"+f.getName()+ f.getType()+"\t"+methodResource.type()+"\t"+methodResource.lookup());
                        }
                    }
                }
            }
        }
    }

    public WebContext setPkg(String pkg) {
        this.pkg = pkg;
        return this;
    }

    public WebContext start() throws Exception {
        this.initialBeans();
        this.scanActionsFromBeans();
        return this;
    }
}
